import Link from "next/link"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import type { ReactNode } from "react"; import { IMPORTANCE_WEIGHTS } from "@websensor/core/client"; import { DiffViewer } from "@/components/diff-viewer"; import { BookmarkButton } from "@/components/event-drawer"; import { EventRow } from "@/components/event-row"; import { FieldChangeInline, FieldChanges } from "@/components/field-changes"; import { Badge, Chip, Empty, EvidenceTag, ExtLink, Flag, Gauge, HealthPill, PageHeader, Panel, Score, StateBadge, Table, Td, TypeChip } from "@/components/ui"; import { ShareButton, WatchButton } from "@/components/watch-button"; import { api, SITE_URL, type SnapshotRow } from "@/lib/api"; import { CLASS_LABELS, fmtBytes, fmtMs, fmtOffset, fmtPct, fmtScore, relTime, shortHash, typeLabel, utcDateTime } from "@/lib/format"; export const dynamic = "force-dynamic"; export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { const { slug } = await params; const d = await api.event(slug); if (!d) return { title: "Event not found" }; const e = d.event; const desc = e.summary.slice(0, 200); return { title: e.title, description: desc, alternates: { canonical: `/event/${e.slug}` }, openGraph: { type: "article", title: e.title, description: desc, url: `${SITE_URL}/event/${e.slug}`, publishedTime: e.detected_at, tags: [e.event_type, ...e.categories] }, twitter: { card: "summary_large_image", title: e.title, description: desc }, }; } function Row({ label, value, tone = "" }: { label: string; value: ReactNode; tone?: string }) { return ( <>
{label}
{value}
); } function Cell({ v, l }: { v: number | string; l: string }) { return (
{v}
{l}
); } export default async function EventPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const d = await api.event(slug); if (!d) notFound(); const e = d.event; const change = e.change_id ? await api.change(e.change_id) : null; const interp = (e.interpretation ?? {}) as Record; const observed = typeof interp.observed === "string" ? interp.observed : null; const inferred = typeof interp.inferred === "string" && interp.inferred.trim() ? interp.inferred : null; const who = typeof interp.who_it_affects === "string" ? interp.who_it_affects : null; const model = typeof interp.model === "string" ? interp.model : "heuristics-v1"; const oldSnap = d.snapshots.find((s) => s.id === e.old_snapshot_id); const newSnap = d.snapshots.find((s) => s.id === e.new_snapshot_id); const subject = e.entities.find((x) => x.role === "subject") ?? e.entities[0]; const signal = e.signal_score ?? e.importance; const cluster = e.cluster; const clusterHref = cluster ? `/cluster/${cluster.slug ?? cluster.id}` : d.cluster ? `/cluster/${d.cluster.slug ?? d.cluster.id}` : null; const leadTime = cluster?.lead_time_ms ?? d.cluster?.lead_time_ms ?? null; const hasFields = (e.field_changes?.length ?? 0) > 0; const diffKind = change?.change.kind ?? d.change?.kind ?? null; const diffSummary = change?.change.diff ?? d.change?.diff ?? null; const defaultTab = diffKind === "text" ? "split" : "semantic"; const reasons = e.score_reasons ?? []; const history = d.history ?? []; const jsonLd = { "@context": "https://schema.org", "@type": "NewsArticle", headline: e.title, description: e.summary, datePublished: e.detected_at, dateModified: e.processed_at ?? e.detected_at, url: `${SITE_URL}/event/${e.slug}`, mainEntityOfPage: `${SITE_URL}/event/${e.slug}`, image: [`${SITE_URL}/event/${e.slug}/opengraph-image`], author: { "@type": "Organization", name: "WebSensor", url: SITE_URL }, publisher: { "@type": "Organization", name: "WebSensor", url: SITE_URL }, about: e.entities.map((x) => ({ "@type": "Thing", name: x.name, url: `${SITE_URL}/entity/${x.id}` })), isBasedOn: e.url, keywords: [e.event_type, ...e.categories, ...e.keywords].join(", "), }; const snapRows: [string, SnapshotRow | undefined][] = [ ["Before", oldSnap], ["After", newSnap], ]; return ( <>